Взгляд тестировщика на SOLID

Привет, Хабр! Меня зовут Оля, и я старший инженер по тестированию в Lineate. Хочу рассказать о своей попытке осознать SOLID принципы и понять, где их место в автоматизированном тестировании. 

Сегодня можно найти тысячи статей о SOLID. Только на Хабре их как минимум пара десятков. Эту я пишу по двум причинам: за время изучения не видела материала, в котором бы все принципы SOLID раскрывались на сквозном примере, и в сети нашла минимум информации про применение SOLID в автоматизации тестирования.

Соответственно, этот материал состоит из двух частей:

  • в первой возьмем простое приложение на Java и улучшим его с помощью SOLID принципов — от программы с парой классов, которые делают все подряд, дойдем до приложения, разбитого на несколько модулей с конкретными функциями (да, это еще одно объяснение SOLID, смело пропускайте, если уже и так все знаете);

  • во второй части посмотрим, где во фреймворках автоматизированного тестирования может использоваться SOLID.

SOLID: базовые факты

SOLID — это пять основных принципов объектно-ориентированного программирования и проектирования кода. 

Вот что стоит за каждой из букв в обозначении:

S — SRP, Single Responsibility Principle (Принцип единственной ответственности)

O — OCP, Open/ Closed Principle (Принцип открытости/ закрытости)

L — LSP, Liskov Substitution Principle (Принцип подстановки Барбары Лисков)

I — ISP, Interface Segregation Principle (Принцип разделения интерфейсов)

D — DIP, Dependency Inversion Principle (Принцип инверсии зависимостей) 

Принципы SOLID не были искусственно придуманы теоретиками, а скорее обобщают коллективные знания и опыт разработчиков. После нескольких десятилетий работы Роберт «Дядюшка Боб» Мартин объединил их в единую концепцию. А звучный акроним SOLID предложил Майкл Физерс. 

Основными бонусами от использования SOLID принципов должны стать:

  • простой и понятный код, который потребует минимального времени на вхождение от нового разработчика;

  • стабильный код, в который можно максимально безболезненно встраивать новые фичи, запрошенные заказчиком;

  • код с низкой связанностью, над которым в параллель могут работать несколько разработчиков;

  • минимальное количество регрессионных багов при внесении изменений в существующий код.

SOLID принципы используются на уровне модулей и классов в программах, построенных в парадигме ООП. 

Существуют и другие подходы к проектированию кода. Это методологии GRASP, DRY, KISS, YAGNI и др. Каждый из подходов имеет свои особенности, но суть одна — сделать код более простым и удобным для активной разработки и поддержки.

Тестовое приложение

Чтобы разобраться, как работают SOLID принципы, я написала маленькое и очень простое приложение на Java. Представим, что этим приложением будут пользоваться оформители интерьера, дизайнеры и строители. Задача программы – вычислять площадь геометрических фигур в чистом виде, а также площадь с небольшим коэффициентом. Первый вариант использования – посчитать чистую площадь пола в помещении для внесения в смету или на эскиз. Второй – рассчитать, сколько нужно купить плитки с небольшим запасом, чтобы покрыть пол, или сколько купить краски, чтобы покрасить стену.

Будем считать, что это развивающийся продукт и на данном этапе он имеет некоторые функции и ограничения. Например, тип фигуры определяется не программно, а пользователем через консоль, как и измерения фигуры, необходимые для вычисления площади. Результат вычислений также выводится в консоль.

Так выглядит структура проекта в самом начале (ветка SRP-1):

В проекте есть пакет models, где лежит enum Figure. Тут перечислены все типы фигур, которые поддерживаются приложением на данный момент. 

Здесь же, в пакете models, лежат объекты самих фигур. К примеру, треугольник выглядит так:

@Data public class Triangle {         private Double baseLength;      private Double height; }

Я использую библиотеку Lombok, поэтому не прописываю явно конструктор, сеттеры и геттеры, они здесь есть, но за счет аннотации скрыты. 

Также в приложении есть несколько классов, в которых описана основная логика. Это все, что касается взаимодействия с пользователем и собственно вычисления площади.

В UserInteraction – методы для общения с пользователем:

public class UserInteraction {     //...       public Figure readFigureFromInput() {        //ask user to enter figure in console and return figure    }     public String readAreaTypeFromInput(Figure figure) {             //ask user to enter area type in console and return area type      }     public void printAreaInConsole(Figure figure, String areaType, Double area) {             //print area in console       } }

Класс CalculateArea – точка входа для вычисления площади. Именно его метод calculateArea(Figure figure, String areaType) вызывается в исполняемом классе Main. В этом методе в зависимости от типа фигуры и типа площади вызываются методы для вычислений. Если нужно посчитать чистую площадь без коэффициентов, используем методы из этого же класса, а если нужна площадь под покраску или для плитки, используем инстанс класса CalculateDecorationArea и его методы для фигур. 

public class CalculateArea {         private CalculateDecorationArea calculateDecorationArea = new CalculateDecorationArea();         public Double calculateArea(Figure figure, String areaType) {          Double area = null;             if (areaType == "simple") {                  if (figure == Figure.CIRCLE) {                        area = calculateCircleArea();                  } else if (figure == Figure.SQUARE) {                        area = calculateSquareArea();                  } else if (figure == Figure.TRIANGLE) {                        area = calculateTriangleArea();                  }            } else if (areaType == "painting") {                  area = calculateDecorationArea.calculateDecorationArea(figure);            } else if (areaType == "tile") {                  area = calculateDecorationArea.calculateDecorationArea(figure);            }       return area;      }         public Double calculateSquareArea() {     //user input and calculations for square      }         public double calculateCircleArea() {           //user input and calculations for circle       }         public double calculateTriangleArea() {           //user input and calculations for triangle   } }

CalculateDecorationArea выглядит очень похоже и отличается только применением коэффициента в формуле расчета площади.

Если попробовать запустить приложение, можно увидеть, что оно вполне нормально работает (дисклеймер: работают только основные кейсы, в коде нет никакой обработки ошибок).

Что здесь не так? Ведь код компилируется, и программа выполняет свои функции. Посмотрим на приложение с точки зрения SOLID принципов.

Окунемся в SOLID на живом примере

Пришло время более подробно познакомиться с каждым из принципов SOLID и посмотреть, как они работают на улучшение кода.

S – SRP, Single Responsibility Principle 

Первый из принципов. Роберт Мартин в своей книге «Чистая архитектура» расшифровывает его так:

Модуль должен иметь одну и только одну причину для изменения

ПО меняется в ответ на запросы пользователей → Модуль должен отвечать за одну и только одну группу пользователей или заинтересованных лиц, то есть за одного актора → Модуль должен отвечать за одного и только одного актора.

Опасность представляют модули и классы, которые обслуживают сразу нескольких потребителей или акторов (это могут быть живые пользователи приложения или другие модули программы) и меняются в зависимости от их требований.

Спустимся на уровень классов. Если класс делает вычисления, что-то куда-то отправляет, выводит, описывает логику логирования – это нарушение SRP и антипаттерн. Такой объект можно назвать «божественный объект» или God object.

Чтобы лучше понять этот принцип, обратимся к тестовому приложению. 

Пример из приложения 

Первый и явный пример – конфликт интересов маляров и плиточников. Класс CalculateDecorationArea содержит код, которым пользуются эти две группы акторов. К каким проблемам это может привести? Например, маляры поймут, что в программе заложен слишком большой коэффициент. Допустим, они захотят его поменять, чтобы при закупке краски не оставалось излишков. Но что если для кафельной плитки такие коэффициенты вполне подходят? Получается, с одной стороны у нас есть маляры, а с другой — укладчики плитки, и обе эти группы пользуются одним и тем же кодом для расчета площади. Если код поменяется с учетом новых требований от маляров, плитки будет закуплено слишком мало. Соответственно, в приложении появится дефект с точки зрения плиточников.

Лучше в этом случае разделить вычисления и создать два разных класса. В ветке SRP-1 как раз появляются два отдельных класса CalculatePaintingArea — для маляров с их коэффициентом и CalculateTileArea – для плиточников. 

Пример для маляров с обновленным коэффициентом:

public class CalculatePaintingArea {          private static final Double PAINTING_COEFFICIENT = 1.1;          public Double calculatePaintingArea(Figure figure) {                  Double paintingArea = null;                  if (figure == Figure.CIRCLE) {                   paintingArea = calculateCirclePaintingArea();     } else if (figure == Figure.SQUARE) {       paintingArea = calculateSquarePaintingArea();     } else if (figure == Figure.TRIANGLE) {       paintingArea = calculateTrianglePaintingArea();     }     return paintingArea;       }          public double calculateSquarePaintingArea() {     //user input and calculations for square with coefficient   }      public double calculateCirclePaintingArea() {     //user input and calculations for square with coefficient   }      public double calculateTrianglePaintingArea() {     Double triangleArea;          Scanner sn = new Scanner(System.in);     System.out.println("Enter the length of the triangle base: ");     Double length = sn.nextDouble();     System.out.println("Enter the length of the triangle height: ");     Double height = sn.nextDouble();          triangleArea = length * height / 2 * PAINTING_COEFFICIENT;     return triangleArea;   } }

Можно сказать,